package fr.upmc.dtgui.reflexion;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.NotFoundException;
import javassist.Translator;

/**
 * <b>MakeTranslator est l'implémentation du Translator de javassist dans le
 * cadre du projet.</b>
 * <p>
 * Cette classe va détecter le chargement d'un robot et lui ajouter ce qu'il
 * lui faut pour son bon fonctionnement au sein de la simulation. De plus, on
 * construira la station de téléopération correspondante au même moment.
 * MakeTranslator fait appel à plusieurs factory externes:
 * <ul>
 * <li>DataThreadFactory, qui va générer les threads de manipulation de 
 * données</li>
 * <li>PanelFactory, qui va générer les Panel (éléments de Swing) des stations 
 * de téléopérations</li>
 * <li>RobotStateDataFactory, qui va générer les structures de données relatives 
 * à l'état du robot</li>
 * <li>TeleoperationBoardFactory, qui va générer la station de téléopération 
 * elle-même</li>
 * <li>AnnotationParser, qui va analyser les annotations du robot</li>
 * </ul>
 * </p>
 * 
 * @see DataThreadFactory
 * @see PanelFactory
 * @see RobotStateDataFactory
 * @see TeleoperationBoardFactory
 * @see AnnotationParser
 * @see javassist.Translator
 * 
 * @author Ayoub Benali, Maxime Sarilar
 */


public class MakeTranslator implements Translator {

	/**
     * La liste des classes déjà chargées, par mesure de précaution.
     * <p>
     * Chaque fois qu'une classe est chargée, son nom est ajoutée à la liste,
     * de manière à ce que s'il y a un problème et que la classe est 
     * rechargée via le même loader, on évite les conflits.
     * <p>
     */
	public static ArrayList<String> loaded = new ArrayList<String>();
	
	/**
     * Le nom de la classe chargée.
     * <p>
     * On a besoin du nom du robot un peu partout, cet attribut est fixé de 
     * manière à éviter de devoir "transporter" le nom à chaque appel de méthode
     * dans les paramètres.
     * <p>
     */
	public static String name;
	
	/**
     * La méthode onLoad.
     * <p>
     * Cette méthode intercepte les classes lors de leur chargement et effectue 
     * des traitements selon leur nature et annotations.
     * </p>
     * 
     * @param pool
     *            Le ClassPool associé à la classe chargée.
     * @param name
     *            Le nom de la classe chargée.
     */
	@Override
	public void onLoad(ClassPool pool, String name) throws NotFoundException,
			CannotCompileException {
		if (loaded.contains(name))
			return;
		else
			loaded.add(name);
		boolean isRobot = false;

		CtClass ct = pool.get(name);
		name = ct.getName();

		//Si la classe World est chargée, on adapte son champ robots:
		if (ct.getName().equals("World")) {
			ct.removeField(ct.getField("robots"));
			CtField robots = new CtField(pool.getCtClass("InstrumentedRobot"),
					"robots", ct);
			ct.addField(robots);
		}

		//S'il s'agit d'une interface on ne fait rien:
		if (ct.isInterface())
			return;
		
		/* On cherche les classes qui implantent une interface relative au 
		 * robot, en évitant les classes station de téléopération.
		 */
		CtClass interfaces[] = ct.getInterfaces();
		for (int i = 0; i < interfaces.length; i++) {
			if(interfaces[i].getName().contains("Teleoperation"))
				return;
			if (interfaces[i].getName().contains("Robot"))
				isRobot = true;
		}

		if (isRobot) { //si l'on a affaire à un robot...
			try {
				//Première chose à faire: parser les annotations
				AnnotationParser.parseAnnotation(ct);
				
				/* Tout robot doit avoir les deux annotations suivantes pour
				 * être considéré comme tel.
				 */
				if (AnnotationParser.isWithActuators
						&& AnnotationParser.isWithSensors) {

					//Si le robot permet à la station de lire son energie :
					if (AnnotationParser.hasGetEnergy) {
						RobotStateDataFactory.createEnergyData(ct, pool);

						CtMethod getEnergyData = new CtMethod(
								pool.getCtClass(ct.getName() + "$EnergyData"),
								"getEnergyData", new CtClass[] {}, ct);
						getEnergyData.setBody("{return new " + ct.getName()
								+ ".EnergyData($0."+
								AnnotationParser.getEnergy+"()) ;}");
						getEnergyData.setModifiers(Modifier.SYNCHRONIZED
								| Modifier.PUBLIC);
						ct.addMethod(getEnergyData);
						/* On a rajouté une méthode (getEnergyData) qui permet
						 * de récuperer l'énergie sous la forme d'une structure
						 * de donnée (plutôt que d'un type primitif).
						 */
					}

					//Même chose pour le steering:
					if (AnnotationParser.hasGetSteering) {
						RobotStateDataFactory.createSteeringData(ct, pool);

						CtMethod getSteeringData = new CtMethod(
								pool.getCtClass(ct.getName() + "$SteeringData"),
								"getSteeringData", new CtClass[] {}, ct);
						getSteeringData.setBody("{return new " + ct.getName()
								+ ".SteeringData($0."+
								AnnotationParser.getSteering+"()) ;}");
						getSteeringData.setModifiers(Modifier.SYNCHRONIZED
								| Modifier.PUBLIC);
						ct.addMethod(getSteeringData);
					}

					//Et enfin pour la vitesse:
					if (AnnotationParser.hasGetSpeed) {
						RobotStateDataFactory.createSpeedData(ct, pool);

						CtMethod getSpeedData = new CtMethod(pool.getCtClass(ct
								.getName() + "$SpeedData"), "getSpeedData",
								new CtClass[] {}, ct);
						getSpeedData.setBody("{return new " + ct.getName()
								+ ".SpeedData($0."+
								AnnotationParser.getSpeed+"()) ;}");
						getSpeedData.setModifiers(Modifier.SYNCHRONIZED
								| Modifier.PUBLIC);
						ct.addMethod(getSpeedData);
					}

					//On vérifie maintenant pour les actuateurs de vitesse:
					if (AnnotationParser.hasSetSpeed) {
						CtClass speedChange = 
								RobotActuatorCommandFactory.createSpeedChange(
										ct, pool);
						CtMethod makeSpeedChange = new CtMethod(
								pool.getCtClass(
									"fr.upmc.dtgui.robot.RobotActuatorCommand"),
								"makeSpeedChange",
								new CtClass[] { CtClass.doubleType }, ct);
						makeSpeedChange.setModifiers(Modifier.STATIC
								| Modifier.PUBLIC);
						makeSpeedChange.setBody("{return new "
								+ speedChange.getName() + "($1) ;}");

						ct.addMethod(makeSpeedChange);
					}

					//... Ainsi que pour les actuateurs de steering:
					if (AnnotationParser.hasSetSteering) {
						CtClass steeringChange = 
							RobotActuatorCommandFactory.createSteeringChange(
									ct, pool);
						CtMethod makeSteeringChange = new CtMethod(
								pool.getCtClass(
								"fr.upmc.dtgui.robot.RobotActuatorCommand"),
								"makeSteeringChange",
								new CtClass[] { CtClass.doubleType }, ct);
						makeSteeringChange.setModifiers(Modifier.STATIC
								| Modifier.PUBLIC);
						makeSteeringChange.setBody("{return new "
								+ steeringChange.getName() + "($1)  ;}");
	
						ct.addMethod(makeSteeringChange);
					}

					/* On considère que tout robot peut-être vu par une station 
					 * de téléopération, et cela requiert qu'il ait une méthode
					 * permettant de récuperer sa position sous la forme d'une 
					 * structure de donnée PositioningData.
					 */
					CtMethod getPositioningData = new CtMethod(
							pool.getCtClass(
							"fr.upmc.dtgui.robot.PositioningData"),
							"getPositioningData", new CtClass[] {}, ct);
					String getPositioningDataBody = "{"
							+ "return new " +
							"fr.upmc.dtgui.robot.PositioningData($0.getX(), " +
							"$0.getY(), $0.getDirection());"
							+ "}";
					getPositioningData.setBody(getPositioningDataBody);
					getPositioningData.setModifiers(Modifier.SYNCHRONIZED
							| Modifier.PUBLIC);
					ct.addMethod(getPositioningData);

					// On rajoute l'interface InstrumentedRobot au robot:
					ct.addInterface(pool
							.getCtClass("fr.upmc.dtgui.robot.InstrumentedRobot"));
					
					/* Il faut créer les threads d'envoi/réception de
					 * donnée du robot:
					 */
					CtClass senderThread = createSenderThread(ct, pool);
					CtField senderField = new CtField(senderThread, "sds", ct);
					ct.addField(senderField);

					CtClass receptorThread = createReceptorThread(ct, pool);
					CtField receptorField = new CtField(receptorThread, "adr",
							ct);
					ct.addField(receptorField);
					

					//On modifie le constructeur pour démarrer les threads:
					ct.getConstructors()[0].insertAfter("$0.sds = new "
							+ senderThread.getName() + "($0);"
							+ "$0.adr = new " + receptorThread.getName()
							+ "($0) ;");

					//On récupère la classe BlockingQueue:
					CtClass blockingQueue = pool
							.getCtClass("java.util.concurrent.BlockingQueue");

					/* On crée la méthode qui permet de récupérer la 
					 * BlockingQueue contenant les données du sensor du robot:
					 */
					CtMethod getSensorDataQueue = new CtMethod(blockingQueue,
							"getSensorDataQueue", new CtClass[] {}, ct);
					String getSensorDataQueueBody = "{"
							+ "return $0.sds.getDataQueue();" + "}";
					getSensorDataQueue.setBody(getSensorDataQueueBody);
					getSensorDataQueue.setModifiers(Modifier.SYNCHRONIZED
							| Modifier.PUBLIC);
					ct.addMethod(getSensorDataQueue);

					/* On crée la méthode qui permet de récupérer la 
					 * BlockingQueue contenant les données qui vont être 
					 * appliquées au robot:
					 */
					CtMethod getActuatorDataQueue = new CtMethod(blockingQueue,
							"getActuatorDataQueue", new CtClass[] {}, ct);
					String getActuatorDataQueueBody = "{"
							+ "return $0.adr.getCommandQueue();" + "}";
					getActuatorDataQueue.setBody(getActuatorDataQueueBody);
					getActuatorDataQueue.setModifiers(Modifier.SYNCHRONIZED
							| Modifier.PUBLIC);
					ct.addMethod(getActuatorDataQueue);

					/* On va maintenant ajouter le code nécessaire au méthodes
					 * run() et start() du robot. On doit d'abord trouver
					 * ces méthodes parmis toutes celles du robot:
					 */
					for (CtMethod m : ct.getMethods()) {
						if (m.getName().equals("run")) {
							// Code à rajouter au début du run():
							String ctRunBefore = 
							(AnnotationParser.hasGetEnergy?
				 "double energyLevel = $0."+AnnotationParser.getEnergy+"();":"")
							+ 
							(AnnotationParser.hasGetSteering?
			   "double steeringAngle=$0."+AnnotationParser.getSteering+"();":"")
							+ 
							(AnnotationParser.hasGetSpeed?
						"double speed = $0."+AnnotationParser.getSpeed+"();":"")
							+ "double x = $0.getX() ;"
							+ "double y = $0.getY() ;"
							+ "double direction = $0.getDirection() ;";
							m.insertBefore(ctRunBefore);

							// Code à rajouter à la ligne 421 du run():
							String ctRunAtBlock1 = 
							(AnnotationParser.hasGetSteering ? 
					"steeringAngle = $0."+AnnotationParser.getSteering+"() ;"
									: "")
							+ (AnnotationParser.hasGetSpeed ? 
								"speed = $0."+AnnotationParser.getSpeed+"() ;"
									: "")
							+ "x = $0.getX() ;"
							+ "y = $0.getY() ;"
							+ "direction = $0.getDirection() ;";
							m.insertAt(421, ctRunAtBlock1);

							// Code à rajouter à la ligne 506 du run():
							String ctRunAtBlock2 = 
									(AnnotationParser.hasGetEnergy ? 
											"$0.setEnergyLevel(energyLevel) ;"
											: "");
							m.insertAt(506, ctRunAtBlock2);
						} else if (m.getName().equals("start")) {
							// Code à rajouter au début du start():
							String ctStartAfter = 
									"$0.sds.start(); $0.adr.start();";
							m.insertAfter(ctStartAfter);
						}
					}
					/* Maintenant que le robot est fin prêt, on doit créer
					 * la classe de station de téléopération correspondante:
					 */
					TeleoperationBoardFactory.createTeleoperationBoardClass(ct,
							pool, AnnotationParser.annotations);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	/**
     * La méthode createSenderThread.
     * <p>
     * Cette méthode se charge de créer lors du chargement du robot le Thread
     * SensorDataSender qui permettra au robot de communiquer son état (sous la
     * forme de structure de données héritant de RobotStateData) à la station de
     * téléopération.
     * </p>
     * 
     * @param pool
     *            Le ClassPool associé à la classe chargée.
     * @param ct
     *            La CtClass du robot.
     * @return
     * 			  La CtClass SensorDataSender générée.
     * @see fr.upmc.dtgui.robot.RobotStateData
     * @see java.lang.Thread
     */
	private CtClass createSenderThread(CtClass ct, ClassPool pool)
			throws Exception {
		CtClass sender = ct.makeNestedClass("SensorDataSender", true);
	
		//La classe retournée est sous classe de Thread:
		sender.setSuperclass(pool.getCtClass("java.lang.Thread"));
		
		CtClass blockingQueue = pool
				.getCtClass("java.util.concurrent.BlockingQueue");
		
		//On rajoute deux champs à la classe retournée:
		CtField dataQueue = new CtField(blockingQueue, "dataQueue", sender);
		CtField lr = new CtField(ct, "lr", sender);
		
		//Qui seront tous deux protected:
		dataQueue.setModifiers(Modifier.PROTECTED);
		lr.setModifiers(Modifier.PROTECTED);
		
		sender.addField(dataQueue);
		sender.addField(lr);

		//Le constructeur de la classe:
		CtConstructor constructor = new CtConstructor(new CtClass[] { ct },
				sender);
		String constructorBody = "{"
				+ "super();"
				+ "$0.lr = $1;"
				+ "$0.dataQueue = "
				+ "new java.util.concurrent.ArrayBlockingQueue(4);"
				+ "}";
		constructor.setBody(constructorBody);

		//La méthode getDataQueue() doit être implantée dans la classe:
		CtMethod getDataQueue = new CtMethod(blockingQueue, "getDataQueue",
				new CtClass[] {}, sender);
		String getDataQueueBody = "{" + "return $0.dataQueue;" + "}";
		getDataQueue.setModifiers(Modifier.SYNCHRONIZED | Modifier.PUBLIC);
		getDataQueue.setBody(getDataQueueBody);

		//Comme il s'agit d'un Thread, il lui faut une méthode run():
		CtMethod run = new CtMethod(CtClass.voidType, "run",
				new CtClass[] {}, sender);
		String runBody = "{"
				+ "while(true){"
				+ "dataQueue.clear();"
				+ (AnnotationParser.hasGetSpeed ? 
						"dataQueue.add(lr.getSpeedData()) ;"
						: "")
				+ "dataQueue.add(lr.getPositioningData());"
				+ (AnnotationParser.hasGetEnergy ? 
						"dataQueue.add(lr.getEnergyData()) ;"
						: "")
				+ (AnnotationParser.hasGetSteering ? "dataQueue.add(new "
						+ ct.getName()
						+ "$SteeringData(lr.steeringAngle));" : "")
				+ "try{" + "Thread.sleep((long)"+AnnotationParser.compatibleActuatorValue+");"
				+ "}catch(InterruptedException e){"
				+ "e.printStackTrace();" + "}" + "}" + "}";
		run.setBody(runBody);
		
		//Comme il s'agit d'un Thread, il lui faut une méthode start():
		CtMethod start = new CtMethod(CtClass.voidType, "start",
				new CtClass[] {}, sender);
		String startBody = "{" + "$0.dataQueue.clear() ;"
				+ "super.start();" + "}";
		start.setBody(startBody);
		start.setModifiers(Modifier.SYNCHRONIZED | Modifier.PUBLIC);

		//On ajoute tout cela à la classe:
		sender.addConstructor(constructor);
		sender.addMethod(getDataQueue);
		sender.addMethod(run);
		sender.addMethod(start);

		/* On finalise la classe pour garantir qu'elle ne sera pas modifiée 
		 * passé cette méthode:
		 */
		
		sender.toClass();
		return sender;
	}

	/**
     * La méthode createReceptorThread.
     * <p>
     * Cette méthode se charge de créer lors du chargement du robot le Thread
     * ActuatorDataReceptor qui permettra au robot de lire les instructions
     * venant de la station de téléopération.
     * </p>
     * 
     * @param pool
     *            Le ClassPool associé à la classe chargée.
     * @param ct
     *            La CtClass du robot.
     * @return
     * 			  La CtClass ActuatorDataReceptor générée.
     * @see fr.upmc.dtgui.robot.RobotActuatorCommand
     * @see java.lang.Thread
     */
	private CtClass createReceptorThread(CtClass ct, ClassPool pool)
			throws CannotCompileException, NotFoundException {
		CtClass receptor = ct.makeNestedClass("ActuatorDataReceptor", true);

		
		//La classe retournée est sous classe de Thread:
		receptor.setSuperclass(pool.getCtClass("java.lang.Thread"));

		CtClass blockingQueue = pool
				.getCtClass("java.util.concurrent.BlockingQueue");
		
		//On rajoute deux champs à la classe retournée:
		CtField commandQueue = new CtField(blockingQueue, "commandQueue",
				receptor);
		commandQueue.setModifiers(Modifier.SYNCHRONIZED);
		receptor.addField(commandQueue);
		CtField lr = new CtField(ct, "lr", receptor);
		receptor.addField(lr);

		//Le constructeur de la classe:
		CtConstructor constructor = new CtConstructor(new CtClass[] { ct },
				receptor);
		constructor
				.setBody("{super(); $0.lr = $1 ;$0.commandQueue =" +
						"new java.util.concurrent.ArrayBlockingQueue(1);}");
		receptor.addConstructor(constructor);

		//La méthode getCommandQueue() doit être implantée dans la classe:
		CtMethod getCommandQueue = new CtMethod(blockingQueue,
				"getCommandQueue", new CtClass[] {}, receptor);
		getCommandQueue.setBody("{return $0.commandQueue;}");
		getCommandQueue.setModifiers(Modifier.SYNCHRONIZED
				| Modifier.PUBLIC);
		receptor.addMethod(getCommandQueue);

		//Comme il s'agit d'un Thread, il lui faut une méthode start():
		CtMethod start = new CtMethod(CtClass.voidType, "start",
				new CtClass[] {}, receptor);
		start.setBody("{$0.commandQueue.clear(); super.start();}");
		start.setModifiers(Modifier.SYNCHRONIZED | Modifier.PUBLIC);
		receptor.addMethod(start);

		//Comme il s'agit d'un Thread, il lui faut une méthode run():
		CtMethod run = new CtMethod(CtClass.voidType, "run",
				new CtClass[] {}, receptor);
		String runBody = "{fr.upmc.dtgui.robot.RobotActuatorCommand rac = null;"
				+ "while (true) {"
				+ "try {"
				+ "rac = (fr.upmc.dtgui.robot.RobotActuatorCommand) " 
				+ "$0.commandQueue.take() ;"
				+ "rac.performOn($0.lr) ;"
				+ "Thread.sleep((long)"+AnnotationParser.compatibleSensorValue+");"
				+ "} catch (InterruptedException e) {"
				+ "e.printStackTrace();" + "}" + "}}";
		run.setBody(runBody);
		receptor.addMethod(run);

		/* On finalise la classe pour garantir qu'elle ne sera pas modifiée 
		 * passé cette méthode:
		 */
		receptor.toClass();
		return receptor;

	}

	@Override
	public void start(ClassPool pool) throws NotFoundException,
			CannotCompileException {
	}

}
